iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 20
0
自我挑戰組

不用前端框架 手把手打造基礎SPA網站系列 第 20

[DAY20]進階應用 - 監聽事件的處理(上篇)

  • 分享至 

  • xImage
  •  

今天要來談談SPA監聽事件的處理,由於篇幅的關係,文章分成上下兩篇,那麼就開始今天的主題吧。我們知道Javascript可以監聽瀏覽器事件,而且可以針對不同事件類型進行監聽。比如要對某個按鈕增加點擊的監聽事件Event Listeners時,可以使用addEventListener

const button = document.querySelector('#button')
//Event Listeners
button.addEventListener('click',function(){
    console.log('button clicked')
})

或是Inline events的onclick事件綁定:

const button = document.querySelector('#button')
//Inline events
button.onclick = function () {
  console.log('button clicked')
}

上面兩種方式都可以對目標加入監聽事件,如果這在SPA中會怎麼實現呢?

「簡單阿~不就在render後等標出現再增加監聽嘛」

用eventListener加入監聽

讓我們試試在SPA裡實做,首先在Post.js裡的render裡加入一個按鈕,為了可以對button目標增加監聽,在元件渲染後的mount裡使用addEventListener加入監聽事件如下:

src/pages/Post.js

export const Post = {
  mount: () => {
    const button = document.querySelector('#button')
    //對button增加監聽
    button.addEventListener('click', function () {
      console.log('button clicked')
    })
  },
  render: () => {
    const content = `
      <div class="container">
        <div class="row">
          <div class="col-md">
            <h1>Post page</h1>
            <div>This is post page.</div>
            <button class="btn btn-primary" id="button">click me</button>
          </div>
        </div>
      </div>
    `
    return App.render(content)
  },
}

來看看這一段執行的結果:

「console有跑出紀錄來,看起來一切正常,沒什麼難嘛」

但如果要對10個目標進行監聽呢?總不會要自己手動增加10個吧?若是有100個不就超級麻煩,這樣以後哪記得自己加了什麼監聽,除了代碼不太好維護,而且你知道的,過多的監聽事件也會影響效能執行。

Event Delegation事件捕捉與冒泡

「那對body增加監聽囉,使用Event Delegation事件的捕捉與冒泡」

好的,讓我們再次看看mount裡如何實做,這次換成對父層元件body增加監聽:

src/pages/Post.js

export const Post = {
  mount: () => {
    //對body增加監聽
    document.querySelector('body').addEventListener('click', function (e) {
      //判斷event target id
      if (e.target.id === 'button') {
        console.log('button clicked')
      }
    })
  },
  render: () => {
  //...

可以發現這裡只針對父層的body新增一個監聽事件,搭配事件捕捉與冒泡,用流程控制判斷觸發事件目標的id,不像前面的例子重複對目標新增事件。當你點擊按鈕後切換頁面回來再次點擊時,發生了奇怪的事情:

「咦?為什麼console的內容會越來越多,而且每切回來一次就多增加一筆?」

這是因為剛剛在mount裡對body增加監聽,每造訪Post頁面就執行一次addEventListener,連同之前增加的監聽都一起累計了。

「那為什麼在多頁開發切換頁面就不會發生這個問題?」

那是因為多頁的網站在切換時javascript生命週期會重新起算,單頁應用可不是這樣子,新增的監聽事件會隨著生命週期延續,切換頁面後還是會繼續存在。

removeEventListener移除監聽事件

這時你依舊不死心,想到了移除監聽事件的方法removeEventListener,如果使用這個方法,把先前增加的eventListener給移除呢?

src/pages/Post.js

export const Post = {
  mount: () => {
    const handler = function (e) {
      //判斷event target id
      if (e.target.id === 'button') {
        console.log('button clicked')
      }
    }
 
    //移除監聽
    document.querySelector('body').removeEventListener('click', handler)
    //對body增加監聽
    document.querySelector('body').addEventListener('click', handler)
  },
  render: () => {
  //...

這是你以為的移除監聽事件:

很不幸地,結果看到原來的eventListener還是一樣會累加,什麼都沒改變。原來removeEventListener其實是要對上次造訪mount裡的handler進行移除監聽,但在這邊獲得的handler是這次造訪新宣告的,移除的監聽handler不一樣造成這樣的結果。

使用Inline events

「那麼使用Inline events的onclick事件綁定handler總可以了吧?」

我們再來看看這會是什麼樣:

src/pages/Post.js

export const Post = {
  mount: () => {
    const handler = function (e) {
      //判斷event target id
      if (e.target.id === 'button') {
        console.log('button clicked')
      }
    }
    //對body綁定監聽
    document.querySelector('body').onclick = handler
  },
  render: () => {
  //….

「這樣總沒問題了吧?」

嗯,現在的確沒問題了。某天主管走到你旁邊,希望你在同一頁做一個新功能,搭配按鈕執行監聽事件。當你自信滿滿地在別的程式碼區段,一樣對body綁定onclick事件,好景不常,你發現之前的監聽事件都失效了,只剩下新功能的onclick事件,因為對同一個目標綁定多個Inline events,最後綁定的Inline events會覆蓋掉之前的。

「呃...難道沒有一個有效的監聽事件方法使用在SPA嗎?」

eventListeners vs. Inline events

透過上述冗長的例子可以發現,這些方法似乎都無法完美實做出來,要嘛針對目標一個一個綁定,程式碼會非常難維護且影響效能,要嘛對body綁定然後使用Inline events,但只要再增加事件綁定會覆蓋掉之前的,到底有沒有什麼方式可以幫我們在SPA裡有效實現事件的監聽呢?

仔細想想看,假設有個專門註冊所有元件監聽事件的方法,當元件內有新增監聽事件時會進行註冊,到了別的頁面會找到前一個元件註冊過的監聽事件並進行移除,這樣是不是結合了不重複綁定及不覆蓋handler這兩種優點呢?

明天我們會繼續談談這個部份,並實做有效處理與管理監聽事件的方法,明天見!

參考資料:
1.從ES6開始的JavaScript學習生活-事件處理
2.addEventListener vs onclick
3.Event Delegation — 事件委派介紹 與 觸發委派的回呼函數
4.Event Delegation 事件委派


上一篇
[DAY19]進階應用 - 元件內部State的應用
下一篇
[DAY21]進階應用 - 監聽事件的處理(下篇)
系列文
不用前端框架 手把手打造基礎SPA網站30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言